package com.example.springbootmybatisdemo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;

import java.util.*;

/**
 * @author xm, huangwei_xm@163.com
 * @version 1.3
 * @description
 * @Copyright 2022- huangwei All rights reserved.
 * @since 2022.04.23
 */
@Intercepts({
        @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})
})
public class LogInterceptor implements Interceptor {
    private final String INSERT = "insert";
    private final String UPDATE = "update";
    private final String DELETE = "delete";
    private final String PRIMARY_TABLE_ID = "primary_table_id";
    private final String OPERATING_TYPE = "log_type";
    private final String DATA_VALUE_FORMAT = "'%s'";
    private final String SEMICOLON = ";";
    private final String COMMA = ",";
    private final String UNKNOWN = "unknown";
    /**
     * insert into %s (%s) values (%s);
     */
    private final String SAVE_OPERATING_RECORD = "insert into %s (%s) values (%s);";


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object po = getPo(invocation);
        if (null == po) {
            return invocation.proceed();
        }
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
        StringBuffer sb = new StringBuffer();
        String oldSql = boundSql.getSql();
        sb.append(oldSql);
        sb.append(SEMICOLON);
        String sql = getSaveOperatingRecordSql(po, getOperatingType(oldSql));
        sb.append(sql);
        sb.append(SEMICOLON);
        BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sb.toString(), boundSql.getParameterMappings()
                , boundSql.getParameterObject());
        MappedStatement newMappedStatement = newMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }
        invocation.getArgs()[0] = newMappedStatement;
        return invocation.proceed();
    }

    /**
     * 获取po参数对象
     *
     * @param invocation
     * @return
     */
    private Object getPo(Invocation invocation) {
        Object po = invocation.getArgs()[1];
        if (po instanceof MapperMethod.ParamMap) {
            Set<Map.Entry<String, Object>> entries = ((Map<String, Object>) po).entrySet();
            for (Map.Entry<String, Object> entry : entries) {
                Object value = entry.getValue();
                OperatingRecord annotation = value.getClass().getAnnotation(OperatingRecord.class);
                if (null != annotation) {
                    po = value;
                    break;
                }
            }
        }
        Class<?> aClass = po.getClass();
        OperatingRecord annotation = aClass.getAnnotation(OperatingRecord.class);
        if (null == annotation) {
            po = null;
        }
        return po;
    }

    /**
     * 获取操作类型
     *
     * @param oldSql
     * @return
     */
    private String getOperatingType(String oldSql) {
        if (null == oldSql) {
            return UNKNOWN;
        }
        String lowerOldSql = oldSql.toLowerCase();
        if (lowerOldSql.contains(INSERT)) {
            return INSERT;
        } else if (lowerOldSql.contains(UPDATE)) {
            return UPDATE;
        } else if (lowerOldSql.contains(DELETE)) {
            return DELETE;
        } else {
            return UNKNOWN;
        }
    }

    private String getSaveOperatingRecordSql(Object obj, String operatingType) throws IllegalAccessException {
        Class<?> aClass = obj.getClass();
        OperatingRecord annotation = aClass.getAnnotation(OperatingRecord.class);
        String tableName = annotation.tableName();
        String idName = annotation.idName();
        boolean isRecordId = annotation.isRecordPrimaryId();
        String[] ignoreFiledArr = annotation.ignoreFiled();
        List<String> ignoreFiledList = new ArrayList<>();
        if (null != ignoreFiledArr) {
            ignoreFiledList = Arrays.asList(ignoreFiledArr);
        }
        Map<String, Object> paramMap = JSON.parseObject(JSON.toJSONString(obj), new TypeReference<Map<String, Object>>() {
        });
        StringBuffer filedSb = new StringBuffer(OPERATING_TYPE);
        StringBuffer paramSb = new StringBuffer(String.format(DATA_VALUE_FORMAT, operatingType));
        for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (idName.equalsIgnoreCase(key)) {
                if (isRecordId) {
                    // 记录id
                    key = PRIMARY_TABLE_ID;
                } else {
                    continue;
                }
            }
            if (null != ignoreFiledList && ignoreFiledList.contains(key)) {
                continue;
            }
            filedSb.append(COMMA);
            filedSb.append(key);
            paramSb.append(COMMA);
            paramSb.append(String.format(DATA_VALUE_FORMAT, null == value ? "" : value.toString()));
        }
        return String.format(SAVE_OPERATING_RECORD, tableName, filedSb, paramSb);
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }


    /**
     * 定义一个内部辅助类，作用是包装 SQL
     */
    class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }

    }

    private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new
                MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }
}
